/**
  *  Event Mixins
  *  (c) 2006 Seth Dillingham <seth.dillingham@gmail.com>
  *
  *  This software is hereby released into the public domain. Do with it as
  *  you please, but with the understanding that it is provided "AS IS" and
  *  without any warranty of any kind.
  *
  *  (But I'd love to be told about where and how this code is being used.)
  **/

/**
 * @class Event.Publisher
 *  Description:
 *    add support (to any object or class) by mixing this class into your own
 *
 *  Requires prototype.js
 *
 *  Usage:
 *    To publish custom events:
 *      1. mix this class with your own via
 *         Object.extend( [your class or prototype], Event.Publisher )
 *      2. post events by calling
 *         this.dispatchEvent( [event name], [data for event] )
 *
 *    To activate and deactivate the event-tracing feature, just call
 *      this.toggleEventsTrace()
 */
Event.Publisher = Class.create();
Object.extend( Event.Publisher,
/** @scope Event.Publisher.prototype */
{
	_ls_event_targets: null,

	_event_source_id: null,

	_fl_trace_events: false,

	getEventSourceId: function() {
		if ( typeof this._event_source_id == 'function' )
			return this._event_source_id();
		else
			return this._event_source_id;
	},
	/**
	 * Private. Finds the secret <A> element that this publisher will use to publish
	 * the event event_name through the DOM.
	 * @param {Object} event_name
	 */
	getEventTarget: function( event_name ) {
		if ( ! this._ls_event_targets )
			this._ls_event_targets = new Array();

		if ( ! this._ls_event_targets[ event_name ] )
			$("ArchetypeEventTechnicalLinksContainer").appendChild(
				this._ls_event_targets[ event_name ] = document.createElement( 'A' )
			);

		return this._ls_event_targets[ event_name ];
	},
	/**
	 * Adds the observer function callback_func for the event event_name
	 * (optionally capturing) to the current event publisher.
	 * Called by Event.Listeners.
	 * @param {String} event_name
	 * @param {Function} callback_func
	 * @param {Boolean} capturing
	 */
	addEventListener: function( event_name, callback_func, capturing ) {
		var targ = this.getEventTarget( event_name );

		Event.observe( targ, 'click', callback_func, capturing );

		if ( this._fl_trace_events ) {
			var data =  {
				publisher: this.getEventSourceId(),
				event_name: event_name,
				listener: callback_func,
				capturing: capturing,
				event_source_proxy: targ
			};

			this.dispatchEvent( 'eventListenerAdded', data, true, true );
		}
	},
	/**
	 * Removes the observer function callback_func (where capturing must match)
	 * from the event publisher.
	 * @param {String} event_name
	 * @param {Function} callback_func
	 * @param {Boolean} capturing
	 */
	removeEventListener: function( event_name, callback_func, capturing ) {
		var targ = this.getEventTarget( event_name );

		Event.stopObserving( targ, 'click', callback_func, capturing );

		if ( this._fl_trace_events ) {
			var data =  {
				publisher: this.getEventSourceId(),
				event_name: event_name,
				listener: callback_func,
				capturing: capturing,
				event_source_proxy: targ
			};

			this.dispatchEvent( 'eventListenerRemoved', data, true, true );
		}
	},
	/**
	 * Post events by calling:  this.dispatchEvent( [event name], [data for event] )
	 * @param {String} event_name
	 * @param {Object} data
	 * @param {Boolean} can_bubble
	 * @param {Boolean} cancelable
	 */
	dispatchEvent: function( event_name, data, can_bubble, cancelable ) {
		var targ = this.getEventTarget( event_name );
		var event_data = {
			event_name: event_name,
			event_target: this,
			data: data ? data : null
		};

		if ( ! can_bubble ) can_bubble = false;
		if ( ! cancelable ) cancelable = false;

		var event = Event.create( event_data, can_bubble, cancelable, true, targ );

		if ( this._fl_trace_events ) {
			if ( event_name.match( /event(?:ListenerAdded|ListenerRemoved|Dispatched|Received)/ ) )
				return;

			var data =  {
				publisher: this.getEventSourceId(),
				event_name: event_name,
				event_data: event_data,
				can_bubble: can_bubble,
				cancelable: cancelable,
				event_source_proxy: targ,
				result: event
			};

			this.dispatchEvent( 'eventDispatched', data, true, true );
		}
	},

	toggleEventsTrace: function() {
		var trace = Event.Tracer.findTracer();

		if ( ! trace || ! this._fl_trace_events ) {
			this._fl_trace_events = true;

			trace = Event.Tracer.startTrace();

			trace.registerPublisher( this );
		}
		else {
			this._fl_trace_events = false;

			if ( trace )
				trace.unregisterPublisher( this );
		}

		return this._fl_trace_events;
	},

	isEventsTraceActive: function() {
		return this._fl_trace_events;
	}
} );

/**
 * @class Event.Listener
 * MIX IN: Event.Listener
 *
 *  Description:
 *    easily add support for receiving totally custom events
 *    (to any object or class) by mixing this class into
 *    your own
 *
 *  Usage:
 *	   To receive custom events:
 *      1. mix this class with your own via
 *         Object.extend( [your class or prototype], EventListener )
 *      2. listen for events by calling (from your object)
 *         this.listen()
 *         (see params for this.listen, below)
 **/
Event.Listener = Class.create();
Object.extend( Event.Listener,
/** @scope Event.Listener.prototype */
{
	_listens: null,
	/**
	 * Private. If a call to listenForEvent or stopListeningForEvent omits the name of the listener function,
	 * getEventHandlerName is used to find the "correct" name of the listener function.
	 *
	 * The "correct" name of the handler function is determined by upper-casing the first character
	 * of the event name, replacing all /[ _]/ with -, camelizing the result,
	 *  and then prepending "on". So if the event's name is "i just did_something", this function would return "onIJustDidSomething", and the listener code would then assume that was the name of the listener function.
	 * @param {String} event_name
	 */
	getEventHandlerName: function( event_name ) {
		var onEvent_name = event_name.split( /[ _]/ ).join( '-' ).camelize();

		return "on" + onEvent_name.charAt( 0 ).toUpperCase() + onEvent_name.substr( 1 );
	},

	/**
	 * Causes the listener to "subscribe" for events of type event_name which are published
	 * by event_source, assuming that event_source is an Event.Publisher.
	 *  @param {Object} event_source
	 *      the object which will generate the events, and which implements (or
	 *      mixes in) the Event.Publisher interface (we need addEventListener)
	 *  @param {String} event_name
	 *      the name of the event for which your object will listen
	 *  @param {Boolean} use_capture
	 *      standard DOM Event API param
	 *  @param {String} onEvent_name
	 *      the name of the method in your object which will be called when the
	 *      event is received if you omit this param, listen will look for a
	 *      function named with the CapitalizedCamelCased name of the event with
	 *      "on" at the front. So, if the event is named "message_received",
	 *      we'll look for a function named "onMessageReceived" You can override
	 *      this behavior by overriding getEventHandlerName in your object.
	 */
	listenForEvent: function( event_source, event_name, use_capture, onEvent_name ) {
		if ( ! onEvent_name )
			onEvent_name = this.getEventHandlerName( event_name );

		if ( ! this._listens ) this._listens = new Array();

		var cb = this[ onEvent_name ].bindAsEventListener( this );
		this._listens.push( [ event_source, event_name, use_capture, onEvent_name, cb ] )

		event_source.addEventListener( event_name, cb, use_capture );
	},
	/**
	 * Unsubscribe the current listener from the events of type
	 * event_name published by event_source.
	 * @param {Object} event_source
	 * @param {String} event_name
	 * @param {Boolean} use_capture
	 * @param {String} onEvent_name
	 */
	stopListeningForEvent: function( event_source, event_name, use_capture, onEvent_name ) {
		if ( ! this._listens ) return false;

		if ( ! onEvent_name )
			onEvent_name = this.getEventHandlerName( event_name );

		var ix_item = -1;
		var ls = this._listens.detect( function( val, ix ) {
			if ( ( val[ 0 ] == event_source )
			  && ( val[ 1 ] == event_name )
			  && ( val[ 2 ] == use_capture )
			  && ( val[ 3 ] == onEvent_name ) ) {
				ix_item = ix;
				return true;
			}
		} );

		if ( ix_item >= 0 ) {
			this._listens.splice( ix_item, 1 );

			event_source.removeEventListener( event_name, ls[ 4 ], use_capture );

			return true;
		}

		return false;
	}
} );

/**
  *  Extensions to Prototype's Event object,
  *  for cleanly creating and dispatching custom events
  *
  *  Called from Event.Publisher
  **/
Object.extend( Event,
/** @scope Event.prototype */
{
	create: function( event_data, can_bubble, cancelable, fl_dispatch, target ) {
		var event;

		if ( document.createEvent ) {  // gecko, safari
			if ( ! can_bubble ) can_bubble = false;
			if ( ! cancelable ) cancelable = false;

			if ( /Konqueror|Safari|KHTML/.test( navigator.userAgent ) ) {
				event = document.createEvent( 'HTMLEvents' )

				event.initEvent( 'click', can_bubble, cancelable );
			}
			else {  // gecko uses MouseEvents
				event = document.createEvent( 'MouseEvents' )

				event.initMouseEvent( "click", can_bubble, cancelable,
				                      window, 0, 0, 0, 0, 0,
				                      false, false, false, false, 0, null );
			}
		}
		else {  // msie
			event = document.createEventObject();
			event.event_type = 'onclick';
		}

		event.event_data = event_data;

		if ( fl_dispatch )
			Event.dispatch( target, event );

		return event;
	},

	dispatch: function( target, event ) {
		if ( document.createEvent )
			return target.dispatchEvent( event );
		else
			return target.fireEvent( ( typeof( event.event_type ) != "undefined" ) ? event.event_type : 'onclick', event );
	}
} );


/**
  *  MIX IN: Event.Broker
  *  (c) 2006 Seth Dillingham <seth.dillingham@gmail.com>
  *
  *  This software is hereby released into the public domain. Do with it as
  *  you please, but with the understanding that it is provided "AS IS" and
  *  without any warranty of any kind.
  *
  *  (But I'd love to be told about where and how this code is being used.)
  **/

/**
 * @class Event.Broker
 *  Description:
 *    Allows listeners to subscribe to event types, and remain
 *    blissfully unaware of the available publishers of those events
 *
 *  Requires prototype.js
 *
 *  Usage:
 *    You'll usually want to create a copy of the Broker object by mixing
 *      it in with one of your own classes as follows:
 *      Object.extend( your_obj, Event.Broker )
 *
 *    Publishers register the event types they send with a call to
 *      your_broker_object.registerEventsPublisher
 *      and "unregister" via your_broker_object.unregisterEventsPublisher
 *
 *    Nothing changes for the event listeners, except they call their
 *      listen() method once for each event type (passing a reference
 *      to the broker) instead of once per event type per publisher
 */
Event.Broker = Class.create();
Object.extend( Event.Broker,
/** @scope Event.Broker.prototype */
{
	_listeners: null,

	_publishers: null,

	_initListenerType: function( event_type ) {
		if ( this._listeners == null )
			this._listeners = {};

		if ( typeof( this._listeners[ event_type ] ) == "undefined" )
			this._listeners[ event_type ] = new Array();
	},

	_initPublisherType: function( event_type ) {
		if ( this._publishers == null )
			this._publishers = {};

		if ( this._publishers[ event_type ] == undefined )
			this._publishers[ event_type ] = new Array();
	},

	/**
	 *  Register a publisher with the broker. Listeners that want the types
	 *  of events produced by this publisher will be subscribed automatically.
	 *
	 *  @param {String or Array} event_types: an event type, or an array of event types,
	 *      which are published by the publisher
	 *  @param {Object} publisher: the publisher object being registered with the broker
	 *      (must mix in Event.Publisher, or implement the same public interface)
	 **/
	registerEventsPublisher: function( event_types, publisher ) {
		if ( typeof( event_types ) != typeof( [] ) )
			event_types = [event_types];

		event_types.each( function( event_type ) {
			this._initPublisherType( event_type );

			this._publishers[ event_type ].push( publisher );

			this._initListenerType( event_type );

			this._listeners[ event_type ].each( function( listener_rec ) {
				publisher.addEventListener( event_type, listener_rec.listener, listener_rec.useCapture );
				}
			);
		}.bind( this ) );
	},

	/**
	 *  Unregister a publisher with the broker. Listeners that had been
	 *  automatically subscribed to the publisher will be un-subscribed.
	 *
	 *  @param {String or Array} event_types: an event type, or an array of event types,
	 *      which are published by the publisher
	 *  @param {Object} publisher: the publisher object being un-registered with the broker
	**/
	unregisterEventsPublisher: function( event_types, publisher ) {
		if ( typeof( event_types ) != typeof( [] ) )
			event_types = [event_types];

		event_types.each( function( event_type ) {
			this._listeners[ event_type ].each( function( listener_rec ) {
				publisher.removeEventListener( event_type, listener_rec.listener, listener_rec.useCapture );
			} );

			var ix = this._publishers[ event_type ].indexOf( publisher );

			if ( ix > -1 )
				this._publishers[ event_type ].splice( ix, 1 );
		}.bind( this ) );

		return;
	},

	/**
	 *  Register a listener with the broker. Causes the listener
	 *  to be automatically registered with all publishers that produce
	 *  the specified event_type.
	 *
	 *  You shouldn't have to  call this from your own code: it's
	 *  called automatically when your listener listens for events
	 *  from the broker.
	 *
	 *  See Event.Listener.listenForEvent
	**/
	addEventListener: function( event_type, event_listener, useCapture ) {
		this._initListenerType( event_type );

		this._listeners[ event_type ].push( {
			listener: event_listener,
			useCapture: useCapture
		} );

		this._initPublisherType( event_type );

		this._publishers[ event_type ].each( function( publisher ) {
			publisher.addEventListener( event_type, event_listener, useCapture );
		} );
	},

	/**
	 *  Un-register a listener with the broker. The listener is
	 *  "unsubscribed" from all publishers of the given event_type
	 *
	 *  You shouldn't have to  call this from your own code: it's
	 *  called automatically when your listener stops listening
	 *  for events from the broker.
	 *
	 *  See Event.Listener.stopListeningForEvent
	**/
	removeEventListener: function( event_type, event_listener, useCapture ) {
		this._publishers[ event_type ].each( function( publisher ) {
			publisher.removeEventListener( event_type, event_listener, useCapture );
		} );

		var ix_listener = -1;

		this._listeners[ event_type ].each( function( listener_rec, ix ) {
			ix_listener = ix;
			throw $break;
		} );

		if ( ix_listener > -1 )
			this._listeners[ event_type ].splice( ix_listener, 1 );

		return;
	},

	/**
	 *  tracing/debugging feature only
	 *
	 *  toggles event tracing on all of the publishers of a given type of event
	 *
	 *  note: this is a first pass at this feature. Since it toggles
	 *  the trace feature, toggling event tracing on two different event_types
	 *  could activate trace for an object via the first event type, and then
	 *  deactivate it with the second event type
	**/
	toggleEventPublishersTrace: function( event_type ) {
		this._initPublisherType( event_type );

		this._publishers[ event_type ].each( function( publisher ) {
			publisher.toggleEventsTrace();
		} );
	}
} );
